At the end of this section, you should be able to:
Identify the core components of a function definition and explain their role (the function() directive, arguments, argument defaults, function body, return value)
Describe the difference between argument matching by position and by name
Write if-else, if-else if-else statements to conditionally execute code
Write your own function to carry out a repeated task
Replicate your function multiple times using map()
12.2 Functions
12.2.1 Why functions?
Getting really good at writing useful and reusable functions is one of the best ways to increase your expertise in data science. It requires a lot of practice.
If you’ve copied and pasted code 3 or more times, it’s time to write a function. Try to avoid repeating yourself.
Reducing errors: Copy + paste + modify is prone to errors (e.g., forgetting to change a variable name)
Efficiency: If you need to update code, you only need to do it one place. This allows reuse of code within and across projects.
Readability: Encapsulating code within a function with a descriptive name makes code more readable.
12.2.2 An example
Consider the following code. What does it do?
df<-tibble( a =rnorm(5), b =rnorm(5), c =rnorm(5), d =rnorm(5),)df|>mutate( a =(a-min(a, na.rm =TRUE))/(max(a, na.rm =TRUE)-min(a, na.rm =TRUE)), b =(b-min(a, na.rm =TRUE))/(max(b, na.rm =TRUE)-min(b, na.rm =TRUE)), c =(c-min(c, na.rm =TRUE))/(max(c, na.rm =TRUE)-min(c, na.rm =TRUE)), d =(d-min(d, na.rm =TRUE))/(max(d, na.rm =TRUE)-min(d, na.rm =TRUE)),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 -0.204 0 0.758
2 0.252 0.796 0.229 0.115
3 0.190 0.235 0.655 0.772
4 1 0.0457 0.157 1
5 0 -0.0266 1 0
You might be able to puzzle out that this rescales each column to have a range from 0 to 1. But did you spot the mistake? (Example from R4DS, and…) When Hadley wrote the code he made an error when copying-and-pasting and forgot to change an a to a b. Preventing exactly this type of mistake is one very good reason to learn how to write functions.
The key to the work above is that we want to repeat a set of code multiple times. The code we want to replicate can be written as:
where █ represents the part of the code that changes each time the function is run.
12.2.3 Parts of a function
To create a function you need three things:
A name. Here we’ll use rescale01() because this function rescales a vector to lie between 0 and 1.
The arguments. The arguments are things that vary across calls and our analysis above tells us that we have just one. We’ll call it x because this is the conventional name for a numeric vector.
The body. The body is the code that’s repeated across all the calls.
Then you create a function by following the template:
df|>mutate( a =rescale01(a), b =rescale01(b), c =rescale01(c), d =rescale01(d),)
# A tibble: 5 × 4
a b c d
<dbl> <dbl> <dbl> <dbl>
1 0.707 0 0 0.758
2 0.252 1 0.229 0.115
3 0.190 0.439 0.655 0.772
4 1 0.250 0.157 1
5 0 0.178 1 0
12.2.4 Ordering and arguments
When calling a function, if you don’t name the arguments, R assumes that you passed them in the order defined inside the function.
my_power<-function(x, y){return(x^y)}my_power(x =2, y =3)
[1] 8
my_power(y =3, x =2)
[1] 8
my_power(2, 3)
[1] 8
my_power(3, 2)
[1] 9
12.3 Argument matching
In general, it is safest to match arguments by name and position for your peace of mind. For functions that you are very familiar with (and know the argument order), it’s okay to just use positional matching.
Error in average1(some_data): argument "remove_nas" is missing, with no default
average1(some_data, remove_nas =TRUE)
[1] 13.4
average2(some_data)
Error in average2(some_data): argument "remove_nas" is missing, with no default
average2(some_data, remove_nas =TRUE)
[1] 13.4
average3(some_data)
[1] 13.4
withoutreturn(): the function returns the last value which gets computed and isn’t stored as an object (using <-).
withreturn(): the function will return an object that is explicitly included in the return() call. (Note: if you (accidentally?) have two return() calls, the function will return the object in the first return() call.)
12.4 Control flow
Often inside functions, you will want to execute code conditionally. In a programming language, control structures are parts of the language that allow you to control what code is executed. By far the most common is the if-else if-else structure.
if(logical_condition){# some code}elseif(other_logical_condition){# some other code}else{# yet more code}
Note that inside the curly else brackets, {}, you can have additional lines of code computing objects or conditions, or you can return desired objects.
You can include as many } else if { conditions as your problem calls for.
Functions that return the same number of rows as the original data frame are good to use inside mutate() and filter(). For example, you might want to capitalize the first word of every string:
Functions that collapse into a single value will work well in the summarize() step of the pipeline. For example, you may want to calculate the coefficient of variation which is the standard deviation divided by the mean.
cv<-function(x, na.rm=FALSE){sd(x, na.rm =na.rm)/mean(x, na.rm =na.rm)}cv(runif(100, min =0, max =50))